package sg.edu.nus.iss.jass.datastore.util;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * <p>Helper Class to transfer data between Map and POJO bean basing on the key/attribute Name</p>
 * This helper class does not provide the ability to recursion nested attributes
 * 
 * @author Wang Dayong
 *
 */
public class PropertiesUtil {

	private static final SimpleDateFormat DT_FMT = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss.S");

	private static final String NULL = "String:null";

	/**
	 * Convert POJO bean to key/value pairs, if there's nested POJO in bean, 
	 * this attribute will be ignored. All errors will be ignored
	 * 
	 * @param bean The POJO object is to be transformed
	 * @return HashMap<String, String> the transformed key/value pairs
	 */
	public static Map<String, String> describeBean(Object bean) {

		PropertyDescriptor[] ps = getBeanInfo(bean);
		Map<String, String> attributes = new HashMap<String, String>();
		if (ps!=null) {
			String name = null;
			
			for (PropertyDescriptor p : ps) {
				name = p.getName();
				if(name.equals("class")){
					continue;
				}
				Method m = p.getReadMethod();
				if(m != null) {
					try {
						Object value = m.invoke(bean);
						
						Type type = p.getPropertyType();
						
						if (type == Date.class && value != null) {
							value = DT_FMT.format((Date) value);
						} else if(type == Calendar.class && value != null){
							value = DT_FMT.format((Calendar) value);
						} else if(type == String.class){
							if(value == null) {
								value = NULL;
							}
						} else if(!isWrappedPrimitive((Class)type) && !((Class)type).isPrimitive()){
							continue;
						}
						
						attributes.put(name, value == null?null:String.valueOf(value));
					} catch (IllegalArgumentException e) {
						e.printStackTrace();
					} catch (IllegalAccessException e) {
						e.printStackTrace();
					} catch (InvocationTargetException e) {
						e.printStackTrace();
					}
				}
			}
		}
		return attributes;
	}

	/**
	 * <p>Assemble POJO bean by given key/value pairs</p>
	 * All errors will be ignored, if the key can't map to any attribute of the POJO bean
	 * 
	 * @param bean The destination bean is to be filled.
	 * @param attributes The source key/value pairs
	 */
	public static void populateBean(Object bean, Map<?, ?> attributes) {

		if (bean==null||attributes==null)
			return;
		PropertyDescriptor[] ps = getBeanInfo(bean);
		for (Map.Entry<?, ?> entry : attributes.entrySet()) {
			PropertyDescriptor p = getPropertyDescriptorByName(ps, String.valueOf(entry.getKey()));
			if (p!=null)
				populateValue(bean, p, entry.getValue());
		}

	}

	/**
	 * <p>Populate the given value to specified property of the bean</p>
	 * 
	 * @param bean The destination bean
	 * @param p The destination property of bean
	 * @param value The value is to populated
	 * 
	 */
	private static void populateValue(Object bean, PropertyDescriptor p, Object value) {

		Method method = p.getWriteMethod();
		if (method!=null) {
			Object convertedValue = convert(p.getPropertyType(), value);
			try {
				method.invoke(bean, convertedValue);
			} catch (IllegalArgumentException e) {
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			} catch (InvocationTargetException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * <p>Get all the properties of given bean</p>
	 * @param bean The bean is desire to retrieve all properties
	 * @return The properties described from given bean
	 */
	private static PropertyDescriptor[] getBeanInfo(Object bean) {

		BeanInfo beanInfo = null;
		try {
			beanInfo = Introspector.getBeanInfo(bean.getClass());
		} catch (IntrospectionException e) {
			e.printStackTrace();
		}
		if (beanInfo!=null) {
			return beanInfo.getPropertyDescriptors();
		} else {
			return null;
		}
	}

	/**
	 * <p>Get Property details by given property name</p>
	 * 
	 * @param ps Source collection of properties.
	 * @param name The specified property name to get
	 * @return The Property descriptor of given name. If can not find in source destination collection, return null
	 */
	private static PropertyDescriptor getPropertyDescriptorByName(PropertyDescriptor[] ps, String name) {

		if (ps==null)
			return null;
		for (PropertyDescriptor p : ps) {
			if (p.getName().equals(name)) {
				return p;
			}
		}
		return null;
	}

	/**
	 * <p>To decide whether a given class is wrapper class of primitive.</p>
	 * 
	 * @param clazz The class needs to test
	 * @return If the class is wrapper of specified class, return true, else return false.
	 */
	private static boolean isWrappedPrimitive(Class clazz) {

		try {
			return ((Class) clazz.getField("TYPE").get(null)).isPrimitive();
		} catch (Exception e) {
			return false;
		}
	}

	/**
	 * <p>Convert source value to desired type with given value</p>
	 * Date or Calendar is convert to string of format: dd/MM/yyyy HH:mm:ss.S
	 * If source value is String and the value is null, the converted object is String and value is String:null
	 * 
	 * @param <T> The desired object type
	 * @param t The desired object
	 * @param origValue The source value to be converted
	 * @return The converted object
	 */
	private static <T> T convert(T t, Object origValue) {

		if (origValue==null)
			return null;
		if (t==String.class) {
			if (origValue.equals(NULL))
				return null;
		}
		String value = origValue.toString();
		if (t==Character.class||t==char.class) {
			if (value==null)
				return null;
			return (T) (new Character(value.charAt(0)));
		}
		if (t==Integer.class||t==int.class) {
			return (T) Integer.valueOf(value);
		}
		if (t==Long.class||t==long.class) {
			return (T) Long.valueOf(value);
		}
		if (t==Double.class||t==double.class) {
			return (T) Double.valueOf(value);
		}
		if (t==Float.class||t==float.class) {
			return (T) Float.valueOf(value);
		}
		if (t==Short.class||t==short.class) {
			return (T) Short.valueOf(value);
		}
		if (t==Boolean.class||t==boolean.class) {
			return (T) Boolean.valueOf(value);
		}
		if (t==Date.class) {
			Date date = null;
			try {
				date = DT_FMT.parse(value);
			} catch (ParseException e) {
				e.printStackTrace();
			}
			return (T) date;
		}
		if (t==Calendar.class) {
			Date date = null;
			try {
				date = DT_FMT.parse(value);
			} catch (ParseException e) {
				e.printStackTrace();
			}
			Calendar cal = Calendar.getInstance();
			cal.setTime(date);
			return (T) cal;
		}
		return (T) origValue;
	}

}
